/*
 * Copyright © 2011-2015 the spray project <http://spray.io>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package spray.routing
package directives

import akka.actor.{ Status, ActorRefFactory }
import scala.concurrent.duration.Duration
import scala.concurrent.{ Promise, ExecutionContext }
import scala.util.{ Success, Failure }
import spray.caching._
import spray.http._
import CacheDirectives._
import HttpHeaders._
import HttpMethods._

// not mixed into spray.routing.Directives due to its dependency on com.googlecode.concurrentlinkedhashmap
trait CachingDirectives {
  import BasicDirectives._
  import RouteDirectives._

  type RouteResponse = Either[Seq[Rejection], HttpResponse]

  /**
   * Wraps its inner Route with caching support using the given [[spray.caching.Cache]] implementation and
   * the in-scope keyer function.
   */
  def cache(csm: CacheSpecMagnet): Directive0 = cachingProhibited | alwaysCache(csm)

  /**
   * Passes only requests to the inner route that explicitly forbid caching with a `Cache-Control` header with either
   * a `no-cache` or `max-age=0` setting.
   */
  def cachingProhibited: Directive0 =
    extract(_.request.headers.exists {
      case x: `Cache-Control` ⇒ x.directives.exists {
        case `no-cache`   ⇒ true
        case `max-age`(0) ⇒ true
        case _            ⇒ false
      }
      case _ ⇒ false
    }).flatMap(if (_) pass else reject)

  /**
   * Wraps its inner Route with caching support using the given [[spray.caching.Cache]] implementation and
   * in-scope keyer function. Note that routes producing streaming responses cannot be wrapped with this directive.
   * Route responses other than HttpResponse or Rejections trigger a "500 Internal Server Error" response.
   */
  def alwaysCache(csm: CacheSpecMagnet): Directive0 = {
    import csm._
    mapInnerRoute { route ⇒
      ctx ⇒
        liftedKeyer(ctx) match {
          case Some(key) ⇒
            responseCache(key) { (promise: Promise[RouteResponse]) ⇒
              route {
                ctx.withRouteResponseHandling {
                  case response: HttpResponse ⇒ promise.success(Right(response))
                  case Rejected(rejections)   ⇒ promise.success(Left(rejections))
                  case Status.Failure(e)      ⇒ promise.failure(e)
                  case x ⇒ promise.failure(new RequestProcessingException(StatusCodes.InternalServerError,
                    s"Route responses other than HttpResponse or Rejections cannot be cached (received: $x)"))
                }
              }
            } onComplete {
              case Success(Right(response))  ⇒ ctx.complete(response)
              case Success(Left(rejections)) ⇒ ctx.reject(rejections: _*)
              case Failure(error)            ⇒ ctx.failWith(error)
            }

          case None ⇒ route(ctx)
        }
    }
  }

  //# route-Cache
  def routeCache(maxCapacity: Int = 500, initialCapacity: Int = 16, timeToLive: Duration = Duration.Inf,
                 timeToIdle: Duration = Duration.Inf): Cache[RouteResponse] =
    LruCache(maxCapacity, initialCapacity, timeToLive, timeToIdle)
  //#
}

object CachingDirectives extends CachingDirectives

trait CacheSpecMagnet {
  def responseCache: Cache[CachingDirectives.RouteResponse]
  def liftedKeyer: RequestContext ⇒ Option[Any]
  implicit def executionContext: ExecutionContext
}

object CacheSpecMagnet {
  implicit def apply(cache: Cache[CachingDirectives.RouteResponse])(implicit keyer: CacheKeyer, factory: ActorRefFactory) = // # CacheSpecMagnet
    new CacheSpecMagnet {
      def responseCache = cache
      def liftedKeyer = keyer.lift
      implicit def executionContext = factory.dispatcher
    }
}

trait CacheKeyer extends (PartialFunction[RequestContext, Any])

object CacheKeyer {
  implicit val Default: CacheKeyer = CacheKeyer {
    case RequestContext(HttpRequest(GET, uri, _, _, _), _, _) ⇒ uri
  }

  def apply(f: PartialFunction[RequestContext, Any]) = new CacheKeyer {
    def isDefinedAt(ctx: RequestContext) = f.isDefinedAt(ctx)
    def apply(ctx: RequestContext) = f(ctx)
  }
}
